// 结合 snabbdom 实现的虚拟下拉列表
// randomList 为随机数据 list，getPageData 为分页接口
import randomList, { getPageData } from "../randomList"

// 1. 导入模块
import { init, h, styleModule, propsModule } from "snabbdom"

// 2. 注册模块
const patch = init([
  styleModule,
  propsModule
])




class ScrollList{
    constructor({ 
        initData,
        docHeight = window.screen.height + "px",
        renderType = "origin",
        pageSize = 20,
        element
    }, callback)
    {
        this.pageNum = 1
        this.pageSize = pageSize
        this.totalSize = 20
        this.fetching = false
        this.fetched = {}
        this.element = element
        this.docHeight = docHeight // 外部同期高度
        this.boxHeight = null // 内部容器高度
        this.liHeight = null  // 单条数据高度
        this.startIndex = 0 // 其实渲染数据位置
        this.listData = []
        this.showLength = pageSize // 首次渲染时，默认展示的数据条数
        this.renderType = renderType //页面渲染类型，"origin" 表示原生，"data" 则返回数据

        if(renderType === "data"){
            // react 或者 vue 可以直接使用页面中已有的容器 Dom
            this.callback = callback
        }

        // 首次渲染
        
        if(initData){
            this.firstRender(initData)
        } else {
            this.getAsyncData().then(data => {
                this.firstRender(data)
            })
        }
    }

    /**
     * 渲染虚拟节点，返回节点数组
     */
    firstRender(data) {
        const { 
            startIndex,
            showLength
        } = this

        this.setListData(data)

        // 首次渲染列表
        this.renderLiList(startIndex, startIndex + showLength)

        // 更新元素高度，更新滚动区域高度
        this.updateData()

        // 防抖监听滚动事件，触发重新渲染DOM
        // const delayScroll = this.throttle(this.onScroll, 500)
        this.doc && this.doc.addEventListener("scroll", e => {
            this.onScroll.call(this, e)
        })
    }

    updateData () {
        if(!this.liHeight){
            const nodes = document.getElementsByClassName("scroll-list-ul-li")
            if(nodes && nodes[0]){
                this.liHeight = nodes[0].clientHeight
                this.boxHeight = this.listData.length * this.liHeight
                this.showLength = parseInt(parseInt(this.docHeight)/this.liHeight)
                this.box.elm.style.height = this.boxHeight + "px"
            }
        } else {
            this.boxHeight = this.listData.length * this.liHeight
            this.box.elm.style.height = this.boxHeight + "px"
        }
    }

    onScroll (e) {
        const scrollTop = e.target.scrollTop 
        const { 
            liHeight, 
            startIndex, 
            showLength,
            pageSize,
            fetched
        } = this

        const currentIndex = parseInt(scrollTop/liHeight)
        this.pageNum = parseInt(currentIndex/showLength) + 2

        // console.log("this:", this)
        const pageNum = this.pageNum
        const preBuffer = startIndex>showLength?showLength:0
        const nxtBuffer = showLength

        // 正在获取数据则不继续
        // 已经获取过数据的页面
        if(fetched[pageNum] === "done"){
            // 向上滑动
            if(startIndex - currentIndex > preBuffer - 2){
                this.onScrollcb(currentIndex, showLength, scrollTop)
            } 

            // 向下滑动，考虑buffer为showLength长度
            if(currentIndex - startIndex > nxtBuffer - 2) {
                this.onScrollcb(currentIndex, showLength, scrollTop)
            }
        } else {
            // 向下滑动：请求数据，如果正在请求数据或者已有缓存就不请求
            if(currentIndex > startIndex && this.fetched[pageNum] !== "fetching") {
                this.fetched[pageNum] = "fetching"
                this.getAsyncData().then(data => {
                    this.setListData(data)
                    this.onScrollcb(currentIndex, showLength, scrollTop)
                    this.fetched[pageNum] = "done"
                })
            }
            // 向下滑动，考虑buffer为showLength长度
            else if(currentIndex - startIndex > nxtBuffer - 2) {
                this.onScrollcb(currentIndex, showLength, scrollTop)
            }
        }
    }

    onScrollcb (currentIndex, showLength, scrollTop) {
        this.renderLiList(currentIndex, currentIndex + showLength, scrollTop)
        this.updateData()
        this.startIndex = currentIndex
    }


    async setListData (data) {
        const list = data.userList.map((item, index) => {
            return {
                className: "scroll-list-ul-li",
                style: {
                    position: "relative"
                },
                data: item
            }
        })
        this.listData = this.listData.concat(list)
        this.totalSize = data.totalSize
        this.fetched[this.pageNum] = true
        // console.log("this.listData:", this.listData)
    }

    getListRange (start, end, scrollTop) {
        if(typeof scrollTop !== "undefined"){
            const { showLength, liHeight } = this

            // 向前冗余N条数据，向后冗余N条数据
            let top = 0
            if(start > showLength){
                top = scrollTop - (liHeight * showLength)
                start = start - showLength
            }
            else{
                top = (scrollTop < liHeight) ? 0 : scrollTop
            }
            end = end + showLength
            // console.log(`${start}-${end}`,end-start)

            let nodes = this.listData.slice(start, end)

            return nodes.map((item, index) => {
                item.style.top = top
                return item
            }) 
        }

        return this.listData.slice(start, end)
    }

    // 更新渲染数据
    renderLiList (start, end, scrollTop) {
        let nodes = this.getListRange(start, end, scrollTop)

        // 原生js或者需要挂载dom的情形
        if(this.renderType === "origin"){
            this.renderToDom(nodes)
        }
        // 不需要挂载dom，只返回数据
        if(this.renderType === "data"){
            this.callback && this.callback(nodes)
        }
    }

    // 以下是渲染方法：
    // 渲染节点到 dom 上，renderType 为 "origin" 的时候调用
    renderToDom(nodes) {
        let { element } = this

        // 挂载新的节点到Dom树上
        if(Array.isArray(nodes)){
            nodes = nodes.map(node => this.createSnabbDom(node))
        }

        if(!this.doc){
            let doc = document.createElement("div")
            doc.className = "scroll-list"
            doc.style.overflow = "auto"
            doc.style.height = this.docHeight
            doc.style.position = "relative"
            element.appendChild(doc)
            this.doc = doc
            
            this.box = document.createElement("ul")
            this.doc.append(this.box)
        }

        const box = h('ul.scroll-list-ul', {
            key: "ul",
            style: {
                height: this.boxHeight + "px" || undefined
            }
        }, nodes)

        this.box = patch(this.box, box)
    }

    createSnabbDom (item) {
        let vnode = h(
            `li.${item.className}`, 
            { 
                key: item.data.id,
                props: {
                    key: item.data.id
                },
                style: { 
                    position: item.style.position,
                    top: item.style.top + "px",
                    height: "50px"
                } 
            },
            [
                h('img', {
                    props:{
                        width: 40,
                        src: "//imgservice.suning.cn/uimg1/sniss/improve/p5aM4VyiYhoelqmL9EyOMw.jpg?format=_is_200w_200h_4e.webp"
                    }
                }),
                h('span',`${item.data.id}，${item.data.name}：${item.data.age}`)
            ]
        )
        return vnode
    }

    // 防抖函数
    throttle (fn, wait=100) {
        if(Object.prototype.toString.call(fn)!=="[object Function]"){
            throw Error("必须传递一个函数作为参数！")
            return
        }

        let timer = null

        return function(){
            timer && clearTimeout(timer)
            timer = setTimeout(()=>{
                fn.call(this, ...arguments)
            }, wait)
        }
    }


    // 模拟异步请求数据
    getAsyncData () { 
        const { pageNum, pageSize } = this
        return new Promise((resolve, reject) => {
            setTimeout(()=>{
                console.log("getAsyncData, pageNum:", pageNum)
                const data = getPageData(pageNum, pageSize)

                resolve(data)

            }, 50)
        })
    }
}


const data = randomList(1000)

new ScrollList({
    renderType: "origin",
    element: document.getElementById("root")
})


// new ScrollList({
//     listData: data.userList,
//     renderType: "data"
// }, (list)=>{
//     console.log("list:", list)
// })


export default ScrollList